package hypeerweb.node;

import hypeerweb.connected.ConnectedDownPointer;
import hypeerweb.connected.ConnectedFold;
import hypeerweb.connected.ConnectedInverseSurrogateFold;
import hypeerweb.connected.ConnectedNeighbor;
import hypeerweb.connected.ConnectedNode;
import hypeerweb.connected.ConnectedSurrogateFold;
import hypeerweb.connected.ConnectedUpPointer;

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;



/**
 * @author Adam Christiansen
 */
public class Connections {

	private Set<ConnectedNode> biggerNeighbors;
	private Set<ConnectedNode> smallerNeighborsWithChild;
	private Set<ConnectedNode> smallerNeighborsWithoutChild;
	private Set<ConnectedNode> upPointers; // inverse surrogate neighbors
	private Set<ConnectedNode> downPointers; // surrogate neighbors
	private ConnectedNode fold;
	private ConnectedNode surrogateFold;
	private ConnectedNode inverseSurrogateFold;
	private WebId webId;

	/**
	 * Default constructor for a Connection.
	 * 
	 * @pre None.
	 * @post No Side Effects.
	 */
	public Connections() {
		this.biggerNeighbors = new TreeSet<ConnectedNode>();
		this.smallerNeighborsWithChild = new TreeSet<ConnectedNode>();
		this.smallerNeighborsWithoutChild = new TreeSet<ConnectedNode>();
		this.upPointers = new TreeSet<ConnectedNode>();
		this.downPointers = new TreeSet<ConnectedNode>();
		this.fold = new ConnectedFold(Node.NULL_NODE);
		this.surrogateFold = new ConnectedSurrogateFold(Node.NULL_NODE);
		this.inverseSurrogateFold = new ConnectedInverseSurrogateFold(
				Node.NULL_NODE);
		this.webId = WebId.NULL_WEB_ID;
	}

	/**
	 * Constructor for a Connection given a specific WebId.
	 * 
	 * @param webId
	 * @pre webID not null.
	 * @post No Side Effects.
	 */
	public Connections(WebId webId) {
		this();
		this.webId = webId;
	}

	/**
	 * Adds the given Node to the appropriate Set.
	 * 
	 * @param newNode
	 *            The Node to add to the appropriate Set.
	 * @pre + newNode is not null. + newNode webId should not equal this Node's
	 *      webId.
	 * @post + The size of biggerNeighbors or smallerNeighborsWithChild or
	 *       smallerNeighborsWithoutChild is one greater than before. + Only one
	 *       of those sets changes size.
	 */
	public void addNeighbor(Node node) {
		ConnectedNode newNode = new ConnectedNeighbor(node);
		if (node.getWebId().getValue() == this.webId.getValue()) {
			throw new ArrayIndexOutOfBoundsException("you are a potato");
		} else if (newNode.getWebId().getValue() > this.webId.getValue()) {
			this.biggerNeighbors.add(newNode);
		} else if (newNode.getState() == NodeState.STANDARD) {
			smallerNeighborsWithChild.add(newNode);
		} else {
			smallerNeighborsWithoutChild.add(newNode);
		}
	}

	/**
	 * Removes the given Node from all of this Connections Sets.
	 * 
	 * @param toRemove
	 *            The Node to be removed from this Connections Sets.
	 * @pre toRemove is not null.
	 * @post The size of all the Sets may decrease by one.
	 */
	public void removeNeighbor(Node toRemove) {
		biggerNeighbors.remove(new ConnectedNeighbor(toRemove));
		smallerNeighborsWithChild.remove(new ConnectedNeighbor(toRemove));
		smallerNeighborsWithoutChild.remove(new ConnectedNeighbor(toRemove));
	}

	/**
	 * **Obvious**
	 * 
	 * @return Set<Node>
	 */
	public Set<Node> getSmallerNeighborsWithChild() {
		return asNodeList(smallerNeighborsWithChild);
	}

	/**
	 * Creates and returns a Set representing a combination of all the Nodes in
	 * all this Connection's Sets.
	 * 
	 * @return Set<Node>
	 */
	public Set<Node> getNeighbors() {
		TreeSet<Node> combinedNeighbors = new TreeSet<Node>();
		combinedNeighbors.addAll(asNodeList(biggerNeighbors));
		combinedNeighbors.addAll(asNodeList(smallerNeighborsWithChild));
		combinedNeighbors.addAll(asNodeList(smallerNeighborsWithoutChild));
		return combinedNeighbors;
	}

	/**
	 * Gets the biggest neighbor from this Connection's Sets.
	 * 
	 * @return Node
	 */
	public Node getBiggestNeighbor() {
		if (!upPointers.isEmpty()) {
			return ((TreeSet<ConnectedNode>) upPointers).last().asNode();
		} else if (!biggerNeighbors.isEmpty()) {
			return ((TreeSet<ConnectedNode>) biggerNeighbors).last().asNode();
		} else if (inverseSurrogateFold.asNode() != Node.NULL_NODE) {
			return inverseSurrogateFold.asNode();
		} else {
			return Node.NULL_NODE;
		}
	}

	/**
	 * Gets the first Node in smallerNeighborsWithoutChild or returns NULL_NODE.
	 * 
	 * @return Node
	 */
	public Node getSmallestNeighborWithoutChild() {
		if (smallerNeighborsWithoutChild.isEmpty()) {
			return Node.NULL_NODE;
		}
		return ((TreeSet<ConnectedNode>) smallerNeighborsWithoutChild).first()
				.asNode();
	}

	/**
	 * Gets the first Node in smallerNeighborsWithChild or returns NULL_NODE.
	 * 
	 * @return Node
	 */
	public Node getSmallestNeighborWithChild() {
		if (smallerNeighborsWithChild.isEmpty()) {
			return Node.NULL_NODE;
		}
		return ((TreeSet<ConnectedNode>) smallerNeighborsWithChild).first()
				.asNode();
	}

	/**
	 * Gets the first Node in downPointers or returns NULL_NODE.
	 * 
	 * @return Node
	 */
	public Node getSmallestSurrogateNeighbor() {
		if (downPointers.isEmpty()) {
			return Node.NULL_NODE;
		}
		return ((TreeSet<ConnectedNode>) downPointers).first().asNode();
	}

	/**
	 * Gets the last Node in upPoiners or returns NULL_NODE.
	 * 
	 * @return Node
	 */
	public Node getBiggestInverseSurrogateNeighbor() {
		if (upPointers.isEmpty()) {
			return Node.NULL_NODE;
		}
		return ((TreeSet<ConnectedNode>) upPointers).last().asNode();
	}

	/**
	 * **Obvious**
	 * 
	 * @return Set<Node>
	 */
	public Set<Node> getUpPointers() {
		return asNodeList(upPointers);
	}

	/**
	 * **Obvious**
	 * 
	 * @param upPointers
	 */
	public void setUpPointers(Set<Node> upPointers) {
		this.downPointers = new TreeSet<ConnectedNode>();
		for (Node node : upPointers) {
			this.upPointers.add(new ConnectedDownPointer(node));
		}
	}

	/**
	 * **Obvious**
	 * 
	 * @return Set<Node>
	 */
	public Set<Node> getDownPointers() {
		return asNodeList(downPointers);
	}

	/**
	 * **Obvious**
	 * 
	 * @param downPointers
	 */
	public void setDownPointers(Set<Node> downPointers) {
		this.downPointers = new TreeSet<ConnectedNode>();
		for (Node node : downPointers) {
			this.downPointers.add(new ConnectedDownPointer(node));
		}
	}

	/**
	 * **Obvious**
	 * 
	 * @return Node
	 */
	public Node getFold() {
		return fold.asNode();
	}

	/**
	 * **Obvious**
	 * 
	 * @param fold
	 */
	public void setFold(Node fold) {
		this.fold = new ConnectedFold(fold);
	}

	/**
	 * **Obvious**
	 */
	public Node getSurrogateFold() {
		return surrogateFold.asNode();
	}

	/**
	 * @param surrogateFold
	 */
	public void setSurrogateFold(Node surrogateFold) {
		this.surrogateFold = new ConnectedSurrogateFold(surrogateFold);
	}

	/**
	 * **Obvious**
	 */
	public Node getInverseSurrogateFold() {
		return inverseSurrogateFold.asNode();
	}

	/**
	 * **Obvious**
	 * 
	 * @param inverseSurrogateFold
	 */
	public void setInverseSurrogateFold(Node inverseSurrogateFold) {
		this.inverseSurrogateFold = new ConnectedInverseSurrogateFold(
				inverseSurrogateFold);
	}

	/**
	 * **Obvious**
	 */
	public WebId getWebId() {
		return webId;
	}

	private TreeSet<Node> asNodeList(Set<ConnectedNode> connectedList) {
		TreeSet<Node> result = new TreeSet<Node>();
		for (ConnectedNode node : connectedList) {
			result.add(node.asNode());
		}
		return result;
	}

	/**
	 * **Obvious**
	 * 
	 * @param webId
	 */
	public void setWebId(WebId webId) {
		this.webId = webId;
	}

	/**
	 * **Obvious**
	 * 
	 */
	public Set<Node> getBiggerNeighbors() {
		return asNodeList(biggerNeighbors);
	}

	/**
	 * Updates this Connection's Sets by re-adding its neighbors
	 * 
	 * @pre This Connection's Sets are out of date
	 * @post This Connection's Sets contain the appropriate Nodes
	 */
	protected void updateChildList() {
		TreeSet<ConnectedNode> tempList1 = new TreeSet<ConnectedNode>(
				smallerNeighborsWithChild);
		TreeSet<ConnectedNode> tempList2 = new TreeSet<ConnectedNode>(
				smallerNeighborsWithoutChild);
		smallerNeighborsWithChild.clear();
		smallerNeighborsWithoutChild.clear();
		for (ConnectedNode c : tempList1) {
			addNeighbor(c.asNode());
		}
		for (ConnectedNode c : tempList2) {
			addNeighbor(c.asNode());
		}
	}
	
	public void updateEveryoneChildList() {
		Iterator<Node> iter = getNodeIterator();
		while(iter.hasNext()){
			Node next = iter.next();
			next.updateChildList();
		}
	}

	public void addUpPointer(Node upPointer) {
		upPointers.add(new ConnectedUpPointer(upPointer));
	}

	public void removeUpPointer(Node upPointer) {
		upPointers.remove(new ConnectedUpPointer(upPointer));
	}

	public void addDownPointer(Node downPointer) {
		downPointers.add(new ConnectedDownPointer(downPointer));
	}

	public void removeDownPointer(Node downPointer) {
		downPointers.remove(new ConnectedDownPointer(downPointer));
	}

	public void removeMe(Node replacementNode) {
		new IterateNodes(this){
			@Override
			public void notify(ConnectedNode connectedNode, Node newNode) {
				connectedNode.removeMe(newNode);
			}
		}.execute(replacementNode);
	}

	public void addMe(Node replacementNode) {
		new IterateNodes(this){
			@Override
			public void notify(ConnectedNode connectedNode, Node newNode) {
				connectedNode.addMe(newNode);
			}
		}.execute(replacementNode);
	}
	
	private Iterator<Node> getNodeIterator(){
		Set<Node> nodes = new TreeSet<Node>();
		nodes.addAll(getNeighbors());
		nodes.addAll(asNodeList(upPointers));
		nodes.addAll(asNodeList(downPointers));
		if(fold.asNode() != Node.NULL_NODE) {
			nodes.add(fold.asNode());
		}
		if(surrogateFold.asNode() != Node.NULL_NODE) {
			nodes.add(surrogateFold.asNode());			
		}
		if(inverseSurrogateFold.asNode() != Node.NULL_NODE) {
			nodes.add(inverseSurrogateFold.asNode());
		}
		return nodes.iterator();
	}
	
	private Iterator<ConnectedNode> getConNodeIterator(){
		Set<ConnectedNode> nodes = new TreeSet<ConnectedNode>();
		nodes.addAll(biggerNeighbors);
		nodes.addAll(smallerNeighborsWithoutChild);
		nodes.addAll(smallerNeighborsWithChild);
		nodes.addAll(upPointers);
		nodes.addAll(downPointers);
		nodes.add(fold);
		nodes.add(surrogateFold);
		nodes.add(inverseSurrogateFold);
		return nodes.iterator();
	}

	private abstract class IterateNodes {
		protected Connections c;
		public IterateNodes(Connections c){
			this.c = c;
		}
		public void execute(Node otherNode){
			Iterator<ConnectedNode> iter = getConNodeIterator();
			while(iter.hasNext()){
				notify(iter.next(), otherNode);
			}
		}
		public abstract void notify(ConnectedNode connectedNode, Node newNode);
	}

	public Node getClosestNodeTo(int target, Node myself) {
		Node closest = myself;
		Iterator<Node> iter = getNodeIterator();
		WebId targetId = new WebId(target,WebId.MAX_HEIGHT);
		int closestDistance = myself.getWebId().distanceTo(targetId);
		while(iter.hasNext()){
			Node next = iter.next();
			if(next.getWebId().getValue() == target) {
				return next;
			}
			int distance = next.getWebId().distanceTo(targetId);
			
			if(distance < closestDistance) {
				closestDistance = distance;
				closest = next;
			}
		}
		return closest;
	};
	public NodeInterface getNodeByWebId(int id) {
		if (fold != null) {
			if (fold.getWebId().getValue() == id) {
				return fold;
			}
		}
		if (surrogateFold != null) {
			if (surrogateFold.getWebId().getValue() == id) {
				return surrogateFold;
			}
		}
		if (inverseSurrogateFold != null) {
			if (inverseSurrogateFold.getWebId().getValue() == id) {
				return inverseSurrogateFold;
			}
		}
		//Now to traverse the collections.
		
		for(ConnectedNode n : biggerNeighbors) {
			if(n.getWebId().getValue() == id) {
				return n;
			}
		}
		for(ConnectedNode n : smallerNeighborsWithChild) {
			if(n.getWebId().getValue() == id) {
				return n;
			}
		}
		for(ConnectedNode n : smallerNeighborsWithoutChild) {
			if(n.getWebId().getValue() == id) {
				return n;
			}
		}
		for(ConnectedNode n : upPointers) {
			if(n.getWebId().getValue() == id) {
				return n;
			}
		}
		for(ConnectedNode n : downPointers) {
			if(n.getWebId().getValue() == id) {
				return n;
			}
		}
		return Node.NULL_NODE;
	}
}
